2.9.6. 编写和使用自己的C库
程序员通常将大型 C 程序划分为相关功能的单独模块(即单独的.c文件)。多个模块共享的定义被放入头文件(.h文件)中,这些文件由需要它们的模块包含。同样,C 库代码也在一个或多个模块(.c文件)和一个或多个头文件(.h文件)中实现。 C 程序员经常实现自己的常用功能 C 库。通过编写库,程序员可以在库中实现该功能一次,然后可以在他们编写的任何后续 C 程序中使用该功能。
在编译, 链接和C库使用部分中,我们介绍了如何使用、编译 C 库代码并将其链接到 C 程序中。在本节中,我们讨论如何用 C 语言编写和使用您自己的库。我们在这里介绍的内容也适用于构造和编译由多个 C 源文件和头文件组成的大型 C 程序。
要在 C 中创建库:
- 在头文件 (
.h) 中定义库的接口。任何想要使用该库的程序都必须包含该头文件。 - 在一个或多个
.c文件中创建该库的实现。这组函数定义实现了库的功能。有些函数可能是库的用户将调用的接口函数,而其他函数可能是库的用户无法调用的内部函数(内部函数是库实现的良好模块化设计的一部分)。 - 编译该库的二进制形式,该库可以链接到使用该库的程序中。
库的二进制形式可以直接从其源文件构建,作为编译使用该库的应用程序代码的一部分。此方法将库文件编译为.o文件并将它们静态链接到二进制可执行文件中。以这种方式包含库通常适用于您为自己使用而编写的库代码(因为您可以访问其.c源文件),并且它也是从多个.c模块构建可执行文件的方法。
或者,可以将库编译为二进制存档 (.a) 或共享对象 (.so) 文件,以供想要使用该库的程序使用。在这些情况下,库的用户通常无法访问库的 C 源代码文件,因此他们无法直接使用使用库源码来编译应用代码。当程序使用此类预编译库(例如.a或.so)时,必须使用gcc的-l命令行选项将库的代码显式链接到可执行文件中。
我们将详细讨论编写、编译和链接库代码的情况,其中程序员可以访问各个库模块(.c或.o文件)。这一重点也适用于设计和编译分为多个.c和.h文件的大型 C 程序。我们简要展示了用于构建归档库(静态库)和共享对象(动态共享库)的命令。有关构建这些类型的库文件的更多信息,请参阅gcc文档,包括gcc和ar的手册页。
库详细信息示例(Library Details by Example)
下面,我们将展示一些创建和使用您自己的库的示例。
定义库接口:
头文件(.h 文件)是包含 C 函数原型和其他定义的文本文件——它们代表库的接口。任何想要使用该库的应用程序中都必须包含头文件。例如,C标准库头文件通常存储在/usr/include/中,可以使用编辑器查看:
$ vi /usr/include/stdio.h
下面是来自库的示例头文件 (mylib.h),其中包含库用户的一些定义。
#ifndef _MYLIB_H_
#define _MYLIB_H_
// a constant definition exported by library:
#define MAX_FOO 20
// a type definition exported by library:
struct foo_struct {
int x;
float y;
};
// a global variable exported by library
// "extern" means that this is not a variable declaration,
// but it defines that a variable named total_times of type
// int exists in the library implementation and is available
// for use by programs using the library.
// It is unusual for a library to export global variables
// to its users, but if it does, it is important that
// extern appears in the definition in the .h file
extern int total_times;
// a function prototype for a function exported by library:
// extern means that this function definition exists
// somewhere else.
/*
* This function returns the larger of two float values
* y, z: the two values
* returns the value of the larger one
*/
extern float bigger(float y, float z);
#endif
头文件通常在其内容周围有特殊的“样板”代码:
#ifndef
// header file contents
#endif
此样板代码可确保编译器的预处理器仅在包含mylib.h的任何 C 文件中包含该内容一次。仅包含一次.h文件内容很重要,可以避免编译时出现重复定义错误(duplicate definition errors)。同样,如果您忘记在使用该库的 C 程序中包含.h文件,编译器将生成未定义符号(undefined symbol)警告。
.h 文件中的注释是库接口的一部分,是为库用户编写的。这些注释应该很详细,解释定义并描述每个库函数的作用、它采用的参数值以及它返回的内容。有时.h文件还会包含描述如何使用该库的顶级注释。
全局变量定义和函数原型之前的关键字extern意味着这些名称是在其他地方定义的。在库导出的任何全局变量之前包含extern尤其重要,因为它将名称和类型定义(在.h文件中)与库实现中的变量声明区分开来。在前面的示例中,全局变量在库内仅声明一次,但它通过库的.h文件中的extern定义导出给库用户。
实现库功能:
程序员在一个或多个.c文件(有时是内部.h文件)中实现库。该实现包括.h文件中所有函数原型的定义以及其实现内部的其他函数。这些内部函数通常使用关键字static定义,这将它们的可见性限制在定义它们的模块(.c文件)内。库实现还应包括.h文件中任何extern全局变量声明的变量定义。这是示例库实现 (mylib.c):
#include <stdlib.h>
// Include the library header file if the implementation needs
// any of its definitions (types or constants, for example.)
// Use " " instead of < > if the mylib.h file is not in a
// default library path with other standard library header
// files (the usual case for library code you write and use.)
#include "mylib.h"
// declare the global variable exported by the library
int total_times = 0;
// include function definitions for each library function:
float bigger(float y, float z) {
total_times++;
if (y > z) {
return y;
}
return z;
}
创建库的二进制形式:
要创建库的二进制形式(.o文件),请使用以下 -c选项进行编译:
$ gcc -o mylib.o -c mylib.c
一个或多个.o文件可以构建库的归档 ( .a) 或共享对象 ( .so) 版本。
-
要构建静态库,请使用归档器 (
ar):ar -rcs libmylib.a mylib.o -
要构建动态链接库,
mylib.o目标文件必须使用位置无关代码(使用-fPIC)构建。 通过将gcc的标志指定为-shared,可以从mylib.o创建libmylib.so共享对象文件:gcc -fPIC -o mylib.o -c mylib.c gcc -shared -o libmylib.so mylib.o -
例如,共享对象和归档库通常是从多个
.o文件构建的(请记住,动态链接库的.o需要使用-fPIC标志构建):gcc -shared -o libbiglib.so file1.o file2.o file3.o file4.o ar -rcs libbiglib.a file1.o file2.o file3.o file4.o
使用并链接库:
.c在使用该库的其他文件中:
#include它的头文件- 在编译期间显式链接到实现(
.o文件)中。
包含库头文件后,您的代码就可以调用库的函数(例如,在 中myprog.c):
#include <stdio.h>
#include "mylib.h" // include library header file
int main(void) {
float val1, val2, ret;
printf("Enter two float values: ");
scanf("%f%f", &val1, &val2);
ret = bigger(val1, val2); // use a library function
printf("%f is the biggest\n", ret);
return 0;
}
`#include` 语法和预处理器
请注意,包含 mylib.h 的 #include 语法与包含 stdio.h 的语法不同。这是因为 mylib.h 未与标准库中的头文件一起定位。预处理器有默认位置来查找标准头文件。当包含具有 <file.h> 语法而不是"file.h"语法的文件时,预处理器会在这些标准位置搜索头文件。
当 mylib.h 包含在双引号内时,预处理器首先在当前目录中查找 mylib.h 文件,然后通过指定 gcc 的包含路径 (-I) 来显式告诉它查找的其他位置。例如,如果头文件位于 /home/me/myincludes 目录中(并且与 myprog.c 文件不在同一目录中),则必须在 gcc 命令行中指定该目录的路径,以便预处理器找到 mylib.h 文件:
$ gcc -I/home/me/myincludes -c myprog.c
常见编译命令(从源码, 目标文件或库文件中构建执行程序)
-
要将使用库 (
mylib.o) 的程序 (myprog.c) 编译为二进制可执行文件:$ gcc -o myprog myprog.c mylib.o -
或者,如果库的实现文件在编译时可用,则可以直接从程序和库
.c文件构建程序:$ gcc -o myprog myprog.c mylib.c -
或者,如果该库可作为归档或共享对象文件使用,则可以使用
-l链接它,(-lmylib:请注意,库名称是libmylib.[a,so],但是仅mylib部分包含在gcc命令行中):$ gcc -o myprog myprog.c -L. -lmylib-L.选项指定libmylib.[so,a]文件的路径(-L后面的.表示应该搜索当前目录)。默认情况下,如果可以找到.so版本,gcc将动态链接库。有关链接和链接路径的详细信息,请参阅 2.9.5. 编译, 链接和C库使用。
然后可以运行该程序:
$ ./myprog
如果您运行 的动态链接版本myprog,您可能会遇到如下错误:
/usr/bin/ld: cannot find -lmylib
collect2: error: ld returned 1 exit status
此错误表明运行时链接器在运行时找不到libmylib.so。要解决此问题,请设置LD_LIBRARY_PATH环境变量以包含libmylib.so文件的路径。 myprog的后续运行使用您添加到LD_LIBRARY_PATH的路径来查找libmylib.so文件并在运行时加载它。例如,如果libmylib.so位于/home/me/mylibs/子目录中,请在 bash shell 提示符下运行此命令(仅一次)以设置LD_LIBRARY_PATH环境变量:
$ export LD_LIBRARY_PATH=/home/me/mylibs:$LD_LIBRARY_PATH